/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jdiameter.api;

import java.io.PrintWriter;
import static java.security.AccessController.doPrivileged;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * <P>The basic service for managing a set of Diameter stacks.<br>
 *
 * <P>As part of its initialization, the <code>DriverManager</code> class will
 * attempt to load the stacks classes referenced in the "diameter.drivers"
 * system property. This allows a user to customize the Diameter Drivers
 * used by their applications. For example in your
 * ~/.hotjava/properties file you might specify:
 * <pre>
 * <CODE>diameter.drivers=foo.bah.Stack:wombat.diameter.Stack</CODE>
 * </pre>
 *
 * A program can also explicitly load Diameter stacks at any time. For
 * example, the my.diameter.Stack is loaded with the following statement:
 * <pre>
 * <CODE>Class.forName("my.diameter.Stack");</CODE>
 * </pre>
 *
 * <P>When the method <code>getSession</code> is called,
 * the <code>StackManager</code> will attempt to
 * locate a suitable stack from amongst those loaded at
 * initialization and those loaded explicitly using the same classloader
 * as the current applet or application.
 * 
 * @author erick.svenson@yahoo.com
 * @version 1.5.1 Final
 */
public final class StackManager {

  private static final Object logSync = new Object();

  private static List<StackInfo> stacks = new CopyOnWriteArrayList<StackInfo>();
  private static PrintWriter logWriter = null;
  private static boolean initialized = false;

  static void initialize() {
    if (initialized) return;
    initialized = true;
    loadInitialStacks();
    println("Diameter StackManager initialized");
  }

  private StackManager() {
  }

  /**
   * Retrieves the log writer.
   *
   * The <code>getLogWriter</code> and <code>setLogWriter</code>
   * methods should be used instead
   * of the <code>get/setlogStream</code> methods, which are deprecated.
   * @return a <code>java.io.PrintWriter</code> object
   * @see #setLogWriter
   */
  public static PrintWriter getLogWriter() {
    synchronized (logSync) {
      return logWriter;
    }
  }
  /**
   * Sets the logging/tracing <code>PrintWriter</code> object
   * that is used by the <code>StackManager</code> and all drivers.
   * <P>
   * There is a minor versioning problem created by the introduction
   * of the method <code>setLogWriter</code>.  The
   * method <code>setLogWriter</code> cannot create a <code>PrintStream</code> object
   * that will be returned by <code>getLogStream</code>
   *
   * @param out the new logging/tracing <code>PrintStream</code> object;
   *      <code>null</code> to disable logging and tracing
   * @throws SecurityException
   *    if a security manager exists and its
   *    <code>checkPermission</code> method denies
   *    setting the log writer
   */
  public static void setLogWriter(java.io.PrintWriter out) {
    synchronized (logSync) {
      logWriter = out;
    }
  }

  /**
   * Attempts to locate a stack.
   * The <code>StackManager</code> attempts to select an appropriate stack from
   * the set of registered Diameter stacks.
   * @param className class name of stack
   * @return stack instance
   * @exception InternalException if a manager has internal error
   */
  public static synchronized Stack getStack(String className) throws InternalException {
    println(new StringBuilder().append("StackManager.getStack(\"").append(className).append("\")").toString());
    if (!initialized) {
      initialize();
    }
    // Gets the classloader of the code that called this method, may be null.
    ClassLoader callerCL = ClassLoader.getSystemClassLoader();
    // Walk through the loaded stacks attempting to locate someone who understands the given URL.
    for (StackInfo di : stacks) {
      // If the caller does not have permission to load the stack then skip it.
      if (getCallerClass(callerCL, di.stackClassName) != di.stackClass) {
        println(new StringBuilder().append("    skipping: ").append(di).toString());
        continue;
      }
      println(new StringBuilder().append("    trying ").append(di).toString());
      if (di.stackClassName.equals(className)) {
        // Success!
        println("geStack returning " + di);
        return (di.stack);
      }
    }

    println("getStack: no suitable stack");
    throw new InternalException("No suitable stack");
  }

  /**
   * Registers the given stack with the <code>ScoketManager</code>.
   * A newly-loaded stack class should call
   * the method <code>registerStack</code> to make itself
   * known to the <code>StackManager</code>.
   *
   * @param stack the new Diameter Stack that is to be registered with the
   *               <code>StackManager</code>
   * @exception InternalException if a manager has internal error
   */
  public static synchronized void registerStack(Stack stack) throws InternalException {
    if (!initialized) {
      initialize();
    }
    StackInfo stackInfo = new StackInfo();
    stackInfo.stack = stack;
    stackInfo.stackClass = stack.getClass();
    stackInfo.stackClassName = stackInfo.stackClass.getName();
    stacks.add(stackInfo);
    println(new StringBuilder().append("registerStack: ").append(stackInfo).toString());
  }

  /**
   * Drops a driver from the <code>DiameterManager</code>'s list.  Applets can only
   * deregister stacks from their own classloaders.
   *
   * @param stack the Diameter stack to drop
   * @exception InternalException if a manager has internal error
   */
  public static synchronized void deregisterStack(Stack stack) throws InternalException {
    // Gets the classloader of the code that called this method, may be null.
    ClassLoader callerCL = ClassLoader.getSystemClassLoader();
    println(new StringBuilder().append("StackManager.deregisterStack: ").append(stack).toString());
    // Walk through the loaded stacks.
    int i;
    StackInfo stackInfo = null;
    for (i = 0; i < stacks.size(); i++) {
      stackInfo = stacks.get(i);
      if (stackInfo.stack == stack)
        break;
    }
    // If we can't find the stack just return.
    if (i >= stacks.size()) {
      println("    couldn't find stack to unload");
      return;
    }
    // If the caller does not have permission to load the stack then throw a security exception.
    if (stackInfo == null || getCallerClass(callerCL, stackInfo.stackClassName) != stackInfo.stackClass) {
      throw new SecurityException();
    }
    // Remove the stack.  Other entries in stacks get shuffled down.
    stacks.remove(i);
  }

  /**
   * Retrieves an Enumeration with all of the currently loaded Diameter stacks
   * to which the current caller has access.
   *
   * <P><B>Note:</B> The classname of a stack can be found using
   * <CODE>d.getClass().getName()</CODE>
   *
   * @return the list of Diameter stacks loaded by the caller's class loader
   */
  public static synchronized Enumeration<Stack> getStacks() {
    List<Stack> result = new CopyOnWriteArrayList<Stack>();
    if (!initialized) {
      initialize();
    }
    // Gets the classloader of the code that called this method, may be null.
    ClassLoader callerCL = ClassLoader.getSystemClassLoader();
    // Walk through the loaded stacks.
    for (StackInfo di : stacks) {
      // If the caller does not have permission to load the stack then skip it.
      if (getCallerClass(callerCL, di.stackClassName) != di.stackClass) {
        println(new StringBuilder().append("    skipping: ").append(di).toString());
        continue;
      }
      result.add(di.stack);
    }

    return Collections.enumeration(result);
  }

  public static void println(String message) {
    synchronized (logSync) {
      if (logWriter != null) {
        logWriter.println(message);
        // automatic flushing is never enabled, so we must do it ourselves
        logWriter.flush();
      }
    }
  }

  private static Class getCallerClass(ClassLoader callerClassLoader, String stackClassName) {
    Class callerC;
    try {
      callerC = Class.forName(stackClassName, true, callerClassLoader);
    } catch (Exception ex) {
      callerC = null;
    }
    return callerC;
  }

  private static void loadInitialStacks() {
    String stacks;
    try {
      stacks = doPrivileged( new GetPropertyAction("diameter.stacks") );
    } catch (Exception ex) {
      stacks = null;
    }
    println(new StringBuilder().append("StackManager.initialize: diameter.stacks = ").append(stacks).toString());
    if (stacks == null)
      return;
    while (stacks.length() != 0) {
      int x = stacks.indexOf(':');
      String stack;
      if (x < 0) {
        stack = stacks;
        stacks = "";
      } else {
        stack = stacks.substring(0, x);
        stacks = stacks.substring(x + 1);
      }
      if (stack.length() == 0)
        continue;
      try {
        println(new StringBuilder().append("StackManager.Initialize: loading ").append(stack).toString());
        Class.forName(stack, true, ClassLoader.getSystemClassLoader());
      } catch (Exception ex) {
        println(new StringBuilder().append("StackManager.Initialize: load failed: ").append(ex).toString());
      }
    }
  }
}

class GetPropertyAction  implements PrivilegedAction<String> {

  private String theProp;
  private String defaultVal;
  public GetPropertyAction(String s) {
    theProp = s;
  }

  public GetPropertyAction(String s, String s1) {
    theProp = s;
    defaultVal = s1;
  }

  public String run() {
    String s = System.getProperty(theProp);
    return s != null ? s : defaultVal;
  }
}

class StackInfo {
  Stack stack;
  Class stackClass;
  String stackClassName;

  public String toString() {
    return (new StringBuilder().append("stack[className=").append(stackClassName).append(",").
        append(stack).append("]").toString());
  }
}


